/*
00000010
00001100
00111110
01010110

00000100
00001000
00001110
00010000
00010110
00011010
00011100
00100110
00101010
00101100
00110010
01000110
01001010
01011000
01011110
01101110

00000010
00000100
00001000
00001110
00010110
00011010
00011100
00100110
00101010
00101100
00110010
00111110
01000110
01001010
01011110
01101110
 */

package Analyser;

import java.util.ArrayList;
import java.util.Hashtable;

public class BarcodeAnalyser {
	
	public ArrayList<Integer> readings = new ArrayList<Integer>();
	private int readSize = 1300; //number of readings to keep for analysis
	private ArrayList<String> barcodes = new ArrayList<String>();
	
	private int brownValue;
	public int whiteMargin=80; //offset from brown value
	public int blackMargin=80; //offset from brown value
	private int BCLength = 8; //number of unit bars in a barcode
	
	public BarcodeAnalyser()
	{
		setupBarcodes();
	}
	
	public void SetBrownValue(int value)
	{
		brownValue = value;
	}
	
	/**
	 * Updates the list of readings with given reading and checks if the list contains a valid barcode.
	 * If so, it returns this detected barcode.
	 * @param reading
	 * @return			The detected barcode, or an empty string.
	 */
	public String Update(int reading)
	{
		if(brownValue == 0) 
			brownValue = reading; //making sure brownValue has been set
		
		readings.add(reading);
		while(readings.size()>readSize)
		{
			readings.remove(0);
		}		
		
		String ret = analyse();
		if(!ret.equals(""))
			readings.clear(); //clear the readings list, so you wont keep sending the same bc over and over again
		
		return ret;
	}
	
	private String analyse()
	{
		//create list with readings of last possible bc reading		
		ArrayList<Integer> data = getLastPossibleReading();
		if(data == null)
			return "";
		

		//analyse this list
		String barcode = calculateBarcode(data);
		String invertedBarcode = invertBarcode(barcode);
		
		if(isValidBarcode(barcode))
			return barcode;
		else if(isValidBarcode(invertedBarcode))
			return barcode;
		else
		{
			String recoveredBC = recover(barcode);
			String recoveredInvertedBC = recover(invertedBarcode);
			
			if(isValidBarcode(recoveredBC))
				return recoveredBC;
			if(isValidBarcode(recoveredInvertedBC))
				return recoveredBC;
		}		
		
		return "";
	}
	
	/**
	 * Returns a list which contains the readings of the last possible read barcode.
	 */
	public ArrayList<Integer> getLastPossibleReading()
	{
		ArrayList<Integer> ret = new ArrayList<Integer>();
		int nbBrownValuesMargin = 25; //the margin of brown values before and after a barcode
		if(readings.size()<nbBrownValuesMargin)
			return null;
		
		for(int i=1; i<nbBrownValuesMargin; i++)
		{
			if(getColorFromValue(readings.get(readings.size()-i)) != 2)
				return null; //you're in the middle of a barcode, no detection possible
		}	
			
		
		//pinch the last brown values from the list (1)
		//add values of the possible barcode to ret (2)
		//stop adding when a lot of brown values are detected again
		//remove last added brown values (3)
		//invert ret (since you started adding from the last reading to the first) (4)
		
		int index = readings.size()-1;
		
		//(1)
		while(getColorFromValue(readings.get(index)) == 2)
		{
			index--;
			if(index <= 0)
				return null;
		}
		
		int nbBrownValues = 0;		
		
		//(2)
		while(nbBrownValues <= nbBrownValuesMargin)
		{
			int c = readings.get(index);
			if(getColorFromValue(c) == 2)
				nbBrownValues++;
			else
				nbBrownValues = 0;
			
			ret.add(c);
			index--;
			if(index < 0)
				return null;
		}
		
		//(3)
		for(int i=0; i<=nbBrownValuesMargin; i++)
		{
			ret.remove(ret.size()-1);
		}
		
		ArrayList<Integer> temp = new ArrayList<Integer>();
		
		//(4)
		for(int i=ret.size()-1; i>=0; i--)
		{
			temp.add(ret.get(i));
		}
		
		ret = temp;		
		
		return ret;
	}
		
	/**
	 * Returns a string which contains a barcode derived from given readings. 
	 * It divides the readings in BCLength equal blocks and calculates the mean of each
	 * block.
	 */
	public String calculateBarcode(ArrayList<Integer> data) {

		int barlength = Math.round(data.size() / BCLength);
		ArrayList<Integer> colors = new ArrayList<Integer>();

		for (int i = 0; i < BCLength; i++) {

			double tempsum = 0;
			for (int j = 0; j < barlength; j++) {
				tempsum += data.get(i * barlength + j);
			}
			double meanValue = tempsum / barlength;
			
			colors.add(getColorFromValueBin((int) meanValue));
		}

		// prepare string to return
		String ret = "";
		for (int i = 0; i < colors.size(); i++) {
			int color = colors.get(i);
			ret = ret + color;
		}

		return ret;
	}
	
	/**
	 * black = 0; white = 1; brown = 2
	 */
	private int getColorFromValue(int value)
	{
		if (value > brownValue+whiteMargin){
			return 1;
		}
		if(value < brownValue-blackMargin){
			return 0;
		}
		else{
			return 2;
		}
	}
	
	/**
	 * black = 0; white = 1
	 */
	private int getColorFromValueBin(int value) {
		// Detected white
		if (value > brownValue)
			return 1;
		// Detected black
		else
			return 0;
	}
	
	private boolean isValidBarcode(String bc)
	{
		for(int i=0; i<barcodes.size(); i++)
		{
			if(barcodes.get(i).equals(bc))
				return true;
		}
		return false;
	}
	
	/**
	 * Inverts the given string and returns the inversion
	 */
	private String invertBarcode(String detectedBarcode) {
		String ret = "";
		for (int i = detectedBarcode.length() - 1; i >= 0; i--) {
			ret += detectedBarcode.charAt(i);
		}
		return ret;
	}
	
	private String recover(String bc) {
		//System.out.println("Recovery 2");

		String recoveredBc = "";
		int minDist = 100;
		for (int i = 0; i < barcodes.size(); i++) {
			int hammingDist = getHammingDist(bc, barcodes.get(i));

			if (hammingDist < minDist) {
				minDist = hammingDist;
				recoveredBc = barcodes.get(i);
			}
		}
		
		if(minDist > 1)
			return "";
				
		return recoveredBc;
	}

	private int getHammingDist(String s1, String s2) {
		int ret = 0;

		for (int i = 0; i < s1.length(); i++) {
			if (s1.charAt(i) != s2.charAt(i))
				ret++;
		}
		return ret;
	}
		
	private void setupBarcodes()
	{
		barcodes.add("01000110");
		barcodes.add("01110110");
		barcodes.add("01011100");
		barcodes.add("01101001");
		barcodes.add("01111000");
		barcodes.add("01110010");
		barcodes.add("01111110");
		barcodes.add("01001100");

	}
	
	public void TestGetLastPossibleReading()
	{
		//setting up data
		brownValue = (int) (255*0.5f);
		
		addRandomValues(brownValue, 20, 30); 	//adding brown
		addRandomValues(220, 20, 100); 			//adding white
		addRandomValues(20, 20, 100); 			//adding black
		addRandomValues(brownValue, 20, 150); 	//adding brown
		addRandomValues(220, 20, 100); 			//adding white
		addRandomValues(20, 20, 100); 			//adding black
		addRandomValues(brownValue, 20, 30); 	//adding brown
				
		
		String readContent = "";
		for(int i=0; i<readings.size();i++)
		{
			readContent += readings.get(i);
			readContent += " ";
		}
		System.out.println("Readings:");
		System.out.println(readContent);
		
		//actual test
		ArrayList<Integer> testResult = getLastPossibleReading();
		
		
		String output = "";
		for(int i=0; i<testResult.size();i++)
		{
			output += testResult.get(i);
			output += " ";
		}		
		System.out.println("Result:");
		System.out.println(output);
		
	}
	
	private void addRandomValues(int center, int offset, int count)
	{
		for(int i=0; i<count; i++)
		{
			if(Math.random()<0.5)
				readings.add((int)(Math.random()*offset + center));
			else
				readings.add((int)(-Math.random()*offset + center));
		}		
	}
	
	public void TestCalculateBarcode()
	{
		//setting up data: "01011010"
		brownValue = (int) (255*0.5f);
		int offset = 20;
		
		addRandomValues(20, offset, 20); 			//adding black
		addRandomValues(brownValue, offset, 10); 	//brown
		addRandomValues(220, offset, 20); 			//adding white
		addRandomValues(brownValue, offset, 10); 	//brown
		addRandomValues(20, offset, 20); 			//adding black
		addRandomValues(brownValue, offset, 10); 	//brown
		addRandomValues(220, offset, 20); 			//adding white
		addRandomValues(brownValue, offset, 10); 	//brown
		addRandomValues(220, offset, 20); 			//adding white
		addRandomValues(brownValue, offset, 10); 	//brown
		addRandomValues(20, offset, 20); 			//adding black
		addRandomValues(brownValue, offset, 10); 	//brown
		addRandomValues(220, offset, 20); 			//adding white
		addRandomValues(brownValue, offset, 10); 	//brown
		addRandomValues(20, offset, 20); 			//adding black
		
		String result = calculateBarcode(readings);
		
		String data = "";
		for(int i=0; i<readings.size(); i++)
		{
			data += readings.get(i);
			data += " ";
		}
		
		System.out.println("Data-input:");
		System.out.println(data);
		System.out.println("Expected: 01011010");
		System.out.println("Calculated: " +result);
	}
	
	public void TestUpdate()
	{
		//setting up data: some brown +01011010+ some brown
		barcodes.add("01011010");
		brownValue = (int) (255*0.5f);
		int offset = 20;
		
		addRandomValues(brownValue, offset, 150); 	//brown
		
		addRandomValues(20, offset, 20); 			//adding black
		addRandomValues(brownValue, offset, 10); 	//brown
		addRandomValues(220, offset, 20); 			//adding white
		addRandomValues(brownValue, offset, 10); 	//brown
		addRandomValues(20, offset, 20); 			//adding black
		addRandomValues(brownValue, offset, 10); 	//brown
		addRandomValues(220, offset, 20); 			//adding white
		addRandomValues(brownValue, offset, 10); 	//brown
		addRandomValues(220, offset, 20); 			//adding white
		addRandomValues(brownValue, offset, 10); 	//brown
		addRandomValues(20, offset, 20); 			//adding black
		addRandomValues(brownValue, offset, 10); 	//brown
		addRandomValues(220, offset, 20); 			//adding white
		addRandomValues(brownValue, offset, 10); 	//brown
		addRandomValues(20, offset, 20); 			//adding black
		
		addRandomValues(brownValue, offset, 100); 	//brown
		
		
		ArrayList<Integer> data = new ArrayList<Integer>();
		for(int i=0; i<readings.size(); i++)
		{
			data.add(readings.get(i));
		}
		
		readings.clear();
				
		for(int i=0; i<data.size(); i++)
		{
			String tempResult = Update(data.get(i));
			if(!tempResult.equals(""))
				System.out.println(tempResult);
		}
		
		System.out.println("Expected: 01011010");
	}
	
	
}
